﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Xml;
using System.Xml.Linq;
using com.bodurov.NearestStarsVisualizerLinesIn3D;

namespace com.bodurov.NearestStarsVisualizer
{
    public class VectorVisualizer
    {
        public const string ConfigFile = "ConfigFile";
        public const int MinStarDiameterInPixels = 5;
        public const int MaxStarDiameterInPixels = 30;
        public const int MinStarDiameterInPixels2 = 1;
        public const int MaxStarDiameterInPixels2 = 4;

        public const int MinBgStarDiameterInPixels = 2;
        public const int MaxBgStarDiameterInPixels = 8;


        private readonly LineGroup _lines;
        private string _configFileName;
        private Ellipse _selectedStar;

        public event EventHandler<EventArgs> ConfigLoaded;
        public IVectorVisualizerUI Page { get; private set; }
        public LineGroup Lines { get { return this._lines; } }

        private readonly List<VectorUI> _vectors = new List<VectorUI>();

        private StarDatabase _db;

        public VectorVisualizer(IVectorVisualizerUI page)
        {
            this.Page = page;
            this._lines = new LineGroup(AxisCenter);
            this.Lines.LineAdded += this.Lines_LineAdded;
            this.Lines.LineRemoved += this.Lines_LineRemoved;
        }

        public void Initialize()
        {
            this.DrawAxes();

            this.DrawSubAxes();

            this.DrawStars();

            this.SetInitialPosition();
        }

        public Brush AxisBrush { get { return this.Page.AxisBrush; } }
        public Brush SubAxisBrush { get { return this.Page.SubAxisBrush; } }
        public double AxisLength { get { return this.Page.AxisLength; } }
        public Point AxisCenter { get { return this.Page.AxisCenter; } }
        public double AxisArrowSize { get { return this.Page.AxisArrowSize; } }
        public double LetterShift { get { return this.Page.LetterShift; } }
        public double LetterWidth { get { return this.Page.LetterWidth; } }
        public double LetterHeight { get { return this.Page.LetterHeight; } }

        public IList<VectorUI> Vectors { get { return this._vectors; } }
        public IEnumerable<Line2D> VectorLines
        {
            get
            {
                foreach(var vectorUI in this.Vectors)
                {
                    yield return vectorUI;
                }
            }
        }

        public StarDatabase Db { get { return this._db; } }

        public void SetInitialPosition()
        {

            this.Lines.RotateToDegrees(20, 40, -13);

            this.Lines.ReDraw();

            this.UpdateCurrrentPage();
        }

        protected virtual void OnConfigLoaded(EventArgs args)
        {
            if (this.ConfigLoaded != null)
            {
                this.ConfigLoaded(this, args);
            }
        }

        //private static double EnsureRange(double value, double limit)
        //{
        //    if (value < 0) value  *= -1;
        //    if (value > limit) return limit;
        //    if (value < (-1*limit)) return -1*limit;
        //    return value;
        //}

        private void AddNearStar(StarInfo starInfo)
        {
            var vectorUI = new VectorUI(this.Page.VectorFactor, starInfo);
            vectorUI.HeadUI.Width = vectorUI.HeadUI.Height = (Db.GetAbsoluteMagnitudeCoef(starInfo)) * (MaxStarDiameterInPixels - MinStarDiameterInPixels) + MinStarDiameterInPixels;
            vectorUI.InitialHeadUiWidth = vectorUI.HeadUI.Width;
            vectorUI.HeadUI2.Width = vectorUI.HeadUI2.Height = (Db.GetAbsoluteMagnitudeCoef(starInfo)) * (MaxStarDiameterInPixels2 - MinStarDiameterInPixels2) + MinStarDiameterInPixels2;
            vectorUI.InitialHeadUi2Width = vectorUI.HeadUI2.Width;
            vectorUI.HeadUI.Fill =
                new RadialGradientBrush(
                    Color.FromArgb((byte)(starInfo.Color.GetA()*0.4), starInfo.Color.GetR(), starInfo.Color.GetG(), starInfo.Color.GetB()),
                    Color.FromArgb(0, starInfo.Color.GetR(), starInfo.Color.GetG(), starInfo.Color.GetB()));
            vectorUI.HeadUI2.Fill =
                new SolidColorBrush(Color.FromArgb(0xFF, starInfo.Color.GetR(), starInfo.Color.GetG(), starInfo.Color.GetB()));

            vectorUI.HeadUI.Stroke = new SolidColorBrush(Color.FromArgb(0, 0, 0, 0));
            
            var point3D = new Point3D(starInfo.DistanceLightYears ?? 0, 0, 0);
            vectorUI.Stroke = this.Page.SubAxisBrush;


            var rotateDegreesByY = GetDegreesRotateByRightAscension(starInfo) * -1;//shift by Y
            var rotateDegreesByZ = GetDegreesRotateByDeclanation(starInfo) * -1;//shift by Z

            point3D = Rotate(point3D, 0, rotateDegreesByY, rotateDegreesByZ);



            var j = 0;
            const double SHIFT = 0.2;
            while (this.Vectors.Find((vUI, i) => vUI.EndX == point3D.X && vUI.EndY == point3D.Y && vUI.EndZ == point3D.Z) != null)
            {
                ++j;
                if(j == 1)
                {
                    point3D.Y += SHIFT;
                }
                else if (j == 2)
                {
                    point3D.Y -= SHIFT / 2;
                    point3D.Z += SHIFT;
                }
                else if (j == 3)
                {
                    point3D.Z -= SHIFT * 2;
                }
                else
                {
                    point3D.Y += SHIFT;
                }
            }

            vectorUI.BeginningX = point3D.X;
            vectorUI.BeginningY = 0;
            vectorUI.BeginningZ = point3D.Z;

            vectorUI.EndX = point3D.X;
            vectorUI.EndY = point3D.Y;
            vectorUI.EndZ = point3D.Z;

            vectorUI.HeadUI.Tag = starInfo;
            vectorUI.HeadUI.MouseEnter += this.HeadUI_MouseEnter;
            vectorUI.HeadUI.MouseLeave += this.HeadUI_MouseLeave;
            vectorUI.HeadUI.MouseLeftButtonDown += this.HeadUI_MouseLeftButtonDown;

            this.AddVector(vectorUI);
        }

        



        private void AddBackgroundStar(StarInfo starInfo)
        {
            var vectorUI = new VectorUI(this.Page.VectorFactor, starInfo);
            vectorUI.HeadUI.Width = vectorUI.HeadUI.Height = (Db.GetApparentMagnitudeCoef(starInfo)) * (MaxBgStarDiameterInPixels - MinBgStarDiameterInPixels) + MinBgStarDiameterInPixels;// min diameter 10 max 40
            vectorUI.InitialHeadUiWidth = vectorUI.HeadUI.Width;
            vectorUI.HeadUI.Fill = new RadialGradientBrush(
                    Color.FromArgb(starInfo.Color.GetA(), starInfo.Color.GetR(), starInfo.Color.GetG(), starInfo.Color.GetB()),
                    Color.FromArgb(0, starInfo.Color.GetR(), starInfo.Color.GetG(), starInfo.Color.GetB()));

           // vectorUI.HeadUI2.Stroke = new SolidColorBrush(Color.FromArgb(0, 0, 0, 0));

            var point3D = new Point3D(this.Page.VectorLimit, 0, 0);

            var rotateDegreesByY = GetDegreesRotateByRightAscension(starInfo) * -1;//shift by Y
            var rotateDegreesByZ = GetDegreesRotateByDeclanation(starInfo) * -1;//shift by Z

            point3D = Rotate(point3D, 0, rotateDegreesByY, rotateDegreesByZ);

            while (this.Vectors.Find((vUI, i) => vUI.EndX == point3D.X && vUI.EndY == point3D.Y && vUI.EndZ == point3D.Z) != null)
            {
                point3D.Y += 0.2;
            }

            vectorUI.EndX = point3D.X;
            vectorUI.EndY = point3D.Y;
            vectorUI.EndZ = point3D.Z;

            vectorUI.HeadUI.Tag = starInfo;

            this.AddVector(vectorUI);

        }

        private int _lineCount;

        private void AddConnectingLine(StarInfo star1, StarInfo star2)
        {
            var point3D_1 = new Point3D(this.Page.VectorLimit * this.Page.VectorFactor, 0, 0);

            var rotateDegreesByY_1 = GetDegreesRotateByRightAscension(star1) * -1;//shift by Y
            var rotateDegreesByZ_1 = GetDegreesRotateByDeclanation(star1);//shift by Z

            point3D_1 = Rotate(point3D_1, 0, rotateDegreesByY_1, rotateDegreesByZ_1);


            var point3D_2 = new Point3D(this.Page.VectorLimit * this.Page.VectorFactor, 0, 0);

            var rotateDegreesByY_2 = GetDegreesRotateByRightAscension(star2) * -1;//shift by Y
            var rotateDegreesByZ_2 = GetDegreesRotateByDeclanation(star2);//shift by Z

            point3D_2 = Rotate(point3D_2, 0, rotateDegreesByY_2, rotateDegreesByZ_2);

            this.Lines.AddLine("StarLine" + (this._lineCount++),
                new[] { point3D_1.X, point3D_1.Y, point3D_1.Z },
                new[] { point3D_2.X, point3D_2.Y, point3D_2.Z });
        }

        private Point3D Rotate(Point3D point3D, double degreesX, double degreesY, double degreesZ)
        {
            var radiansX = Angle.DegreesToRadians(degreesX);
            var radiansY = Angle.DegreesToRadians(degreesY);
            var radiansZ = Angle.DegreesToRadians(degreesZ);

            var matrix = Matrix3D.NewRotateAroundZ(radiansZ);
            matrix = matrix * Matrix3D.NewRotateAroundY(radiansY);
            matrix = matrix * Matrix3D.NewRotateAroundX(radiansX);
            return matrix * point3D;
        }

        private static double GetDegreesRotateByRightAscension(StarInfo starInfo)
        {
            var secs24h = (starInfo.RightAscensionH*60*60)+(starInfo.RightAscensionM*60)+(starInfo.RightAscensionS);
            var secs360 = secs24h*15;
            return secs360 / 3600.0;
        }

        private static double GetDegreesRotateByDeclanation(StarInfo starInfo)
        {
            var secs360 = (starInfo.DeclinationD * 60 * 60) + (starInfo.DeclinationM * 60) + (starInfo.DeclinationS);
            return secs360 / 3600.0;
        }

        private void HeadUI_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            var starUI = (Ellipse)sender;
            this._selectedStar = starUI;
            e.Handled = true;
        }

        private void HeadUI_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if(this._selectedStar != null)
            {
                return;
            }
            var starUI = (Ellipse)sender;
            starUI.Stroke = new SolidColorBrush(Color.FromArgb(0,0,0,0));

            this.Page.HideInfo();
        }

        private void HeadUI_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (this.Page.IsDragging)
            {
                return;
            }
            if (this._selectedStar != null)
            {
                this._selectedStar.Stroke = new SolidColorBrush(Color.FromArgb(0, 0, 0, 0));
                this._selectedStar = null;
            }

            var starUI = (Ellipse)sender;
            var starInfo = (StarInfo)starUI.Tag;

            
            this.Page.ShowInfo(starInfo.ToInfo());

            starUI.Stroke = new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x33, 0));
        }

        public void MouseLeftButtonDown()
        {
            
        }

        public void MouseLeftButtonUp()
        {

        }


        public void AddVector(VectorUI vector)
        {
            //while (this.Vectors.Find((vUI, i) => vUI.Equals(vector)) != null)
            //{
            //    vector.EndY += 5;
            //    vector.EndZ += 5;
            //}
            vector.ID = "Vector" + this.Vectors.Count;
            
            this.Vectors.Add(vector);

            this.Lines.AddLine(vector);

            this.UpdateCurrrentPage();

            this.Lines.ReDraw();
        }

        public void UpdateCurrrentPage()
        {
            this.Page.CurrentPageUrl = this.Vectors.ToLink(HtmlPage.Document.DocumentUri.AbsoluteUri);
        }

        private void Lines_LineAdded(object sender, LineChangedEventArgs e)
        {
            this.Page.AddLine(e.Line);
        }

        private void Lines_LineRemoved(object sender, LineChangedEventArgs e)
        {
            this.Page.RemoveLine(e.Line);
        }

        


        private void DrawAxes()
        {

            this.Lines.DefaultBrush = AxisBrush;

            // draw X axis
            this.Lines.AddLine("X0", 
                new []{0D, 0, 0}, 
                new []{AxisLength, 0, 0});
            // draw X axis arrow
            this.Lines.AddLine("X0_Arrow1",
                new[] { AxisLength, 0, 0 },
                new[] { AxisLength - AxisArrowSize, AxisArrowSize * -1, 0 });
            this.Lines.AddLine("X0_Arrow2", 
                new[] { AxisLength, 0, 0 },
                new[] { AxisLength - AxisArrowSize, AxisArrowSize, 0 });
            // draw X letter
            /*this.Lines.AddLine("X0_Letter1",
                new[] { AxisLength + LetterShift, AxisArrowSize * -1, 0 },
                new[] { AxisLength + LetterShift + LetterWidth, LetterHeight / 2D, 0 });
            this.Lines.AddLine("X0_Letter2",
                new[] { AxisLength + LetterShift, AxisArrowSize, 0 },
                new[] { AxisLength + LetterShift + LetterWidth, (LetterHeight / 2D) * -1, 0 });*/


            // draw Y axis
            this.Lines.AddLine("Y0", 
                new[] { 0D, 0, 0 }, 
                new[] { 0, AxisLength * -1, 0 });
            // draw Y axis arrow
            this.Lines.AddLine("Y0_Arrow1", 
                new[] { 0, AxisLength * -1, 0 },
                new[] { AxisArrowSize * -1, AxisLength * -1 + AxisArrowSize, 0 });
            this.Lines.AddLine("Y0_Arrow2", 
                new[] { 0, AxisLength * -1, 0 },
                new[] { AxisArrowSize, AxisLength * -1 + AxisArrowSize, 0 });
            // draw Y letter
            /*this.Lines.AddLine("Y0_Letter1",
                new[] { 0 + LetterWidth / 2, AxisLength * -1 - (LetterShift + LetterHeight), 0 },
                new[] { 0, AxisLength * -1 - (LetterShift + LetterHeight / 2), 0 });
            this.Lines.AddLine("Y0_Letter2",
                new[] { 0 - LetterWidth / 2, AxisLength * -1 - (LetterShift + LetterHeight), 0 },
                new[] { 0, AxisLength * -1 - (LetterShift + LetterHeight/2), 0 });
            this.Lines.AddLine("Y0_Letter3",
                new[] { 0, AxisLength * -1 - (LetterShift + LetterHeight / 2), 0 },
                new[] { 0, AxisLength * -1 - LetterShift, 0 });*/


            // draw Z axis
            this.Lines.AddLine("Z0", 
                new[] { 0D, 0, 0 }, 
                new[] { 0, 0, AxisLength });
            // draw Z axis arrow
            this.Lines.AddLine("Z0_Arrow1", 
                new[] { 0, 0, AxisLength },
                new[] { 0, AxisArrowSize * -1, AxisLength - AxisArrowSize });
            this.Lines.AddLine("Z0_Arrow2", 
                new[] { 0, 0, AxisLength },
                new[] { 0, AxisArrowSize, AxisLength - AxisArrowSize });
            // draw Z letter
            /*this.Lines.AddLine("Z0_Letter1",
                new[] { 0, 0 - LetterHeight / 2, AxisLength + (LetterShift + LetterWidth) },
                new[] { 0, 0 - LetterHeight / 2, AxisLength + LetterShift });
            this.Lines.AddLine("Z0_Letter2",
                new[] { 0, 0 - LetterHeight / 2, AxisLength + LetterShift },
                new[] { 0, 0 + LetterHeight / 2, AxisLength + (LetterShift + LetterWidth) });
            this.Lines.AddLine("Z0_Letter3",
                new[] { 0, 0 + LetterHeight / 2, AxisLength + (LetterShift + LetterWidth) },
                new[] { 0, 0 + LetterHeight / 2, AxisLength + LetterShift });*/

        }

        private void DrawSubAxes()
        {
            this.Lines.DefaultBrush = SubAxisBrush;

            this.Lines.AddLine("X-0",
                new[] { AxisLength * -1, 0, 0 },
                new[] { 0D, 0, 0 });


            this.Lines.AddLine("Y-0",
                new[] { 0D, AxisLength, 0 },
                new[] { 0D, 0, 0 });

            this.Lines.AddLine("Z-0",
                new[] { 0D, 0, AxisLength * -1 },
                new[] { 0D, 0, 0 });


            var step = (int)this.Page.VectorFactor;
            var axisLength = (AxisLength - AxisArrowSize - LetterShift);

            var subAxisLength = axisLength;

            for (var i = -axisLength - step; (i + step) < axisLength; i += step)
            {
                // sub axes starting from X axes
                //this.Lines.AddLine("XY" + ((i / step) + 1),
                //    new[] { i + step, 0, 0D },
                //    new[] { i + step, subAxisLength * -1, 0D });

                this.Lines.AddLine("XZ" + ((i / step) + 1),
                    new[] { i + step, 0, -subAxisLength },
                    new[] { i + step, 0, subAxisLength });

                // sub axes starting from Y axes
                //this.Lines.AddLine("YX" + ((i / step) + 1),
                //    new[] { -subAxisLength, i * -1 - step, 0D },
                //    new[] { subAxisLength, i * -1 - step, 0D });

                //this.Lines.AddLine("YZ" + ((i / step) + 1),
                //    new[] { 0D, i * -1 - step, 0D },
                //    new[] { 0D, i * -1 - step, subAxisLength });

                // sub axes starting from Z axes
                //this.Lines.AddLine("ZX" + ((i / step) + 1),
                //    new[] { 0D, subAxisLength, i + step },
                //    new[] { 0D, -subAxisLength, i + step });

                this.Lines.AddLine("ZY" + ((i / step) + 1),
                    new[] { -subAxisLength, 0D, i + step },
                    new[] { subAxisLength, 0D, i + step });
            }



            /*
            var step = (int)this.Page.VectorFactor;
            var axisLength = (AxisLength - AxisArrowSize - LetterShift);

            var subAxisLength = axisLength;

            for (var i = 0; (i + step) < axisLength; i += step)
            {
                // sub axes starting from X axes
                this.Lines.AddLine("XY" + ((i / step) + 1),
                    new[] { i + step, 0, 0D },
                    new[] { i + step, subAxisLength * -1, 0D });

                this.Lines.AddLine("XZ" + ((i / step) + 1),
                    new[] { i + step, 0, 0D },
                    new[] { i + step, 0, subAxisLength });

                // sub axes starting from Y axes
                this.Lines.AddLine("YX" + ((i / step) + 1),
                    new[] { 0, i * -1 - step, 0D },
                    new[] { subAxisLength, i * -1 - step, 0D });

                this.Lines.AddLine("YZ" + ((i / step) + 1),
                    new[] { 0D, i * -1 - step, 0D },
                    new[] { 0D, i * -1 - step, subAxisLength });

                // sub axes starting from Z axes
                this.Lines.AddLine("ZX" + ((i / step) + 1),
                    new[] { 0D, 0, i + step },
                    new[] { 0D, subAxisLength * -1, i + step });

                this.Lines.AddLine("ZY" + ((i / step) + 1),
                    new[] { 0D, 0D, i + step },
                    new[] { subAxisLength, 0D, i + step });
            }
             */
        }

        private void DrawStars()
        {
            foreach (var starInfo in this.Db.Stars)
            {
                if(starInfo.IsNearStar)
                    this.AddNearStar(starInfo);
                else
                    this.AddBackgroundStar(starInfo);
            }

            this.Lines.DefaultBrush = new SolidColorBrush(Color.FromArgb(0x80, 0x80, 0x80, 0x80));
            
            
            foreach (var line in this.Db.Lines)
            {
                this.AddConnectingLine(line.Key, line.Value);
            }
        }




        public void MouseMove(double xChange, double yChange)
        {
            // here X is Y and Y is X because moving along X rotates around Y and moving along Y rotates around X
            // Y rotation is multiplied by -1 because 0,0 point on the screen is at the top and not at the bottom
            this.Lines.ShiftDegrees(yChange, xChange * -1, 0);
            this.Lines.ReDraw();
        }


        public void ClearVectors()
        {
            foreach(var line in this.VectorLines)
            {
                this.Lines.RemoveLine(line);
            }
            this.Vectors.Clear();
            this.Page.ResetColorIndex();
            this.UpdateCurrrentPage();
        }


        public void LoadConfiguration(IDictionary<string, string> settingsList)
        {
            var culture = new CultureInfo("en-US");
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = culture;

            string configFile;
            if (!settingsList.TryGetValue(ConfigFile, out configFile))
            {
                // set default value
                configFile = "stars.xml";
            }
            this._configFileName = configFile;
            var url = new Uri(configFile+"?r="+DateTime.Now.Ticks, UriKind.Relative);
            var client = new WebClient();
            client.DownloadStringCompleted += this.client_DownloadStringCompleted;
            client.DownloadStringAsync(url);
        }

        private void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                HtmlPage.Window.Alert("Cannot load configuration file \"" + this._configFileName + "\" expected to be in the 'ClientBin' folder");
                return;
            }
            
            var xmlString = e.Result;

            try
            {
                var xml = XDocument.Parse(xmlString);

                this._db = ParseXml(xml);
            }
            catch(Exception ex)
            {
                HtmlPage.Window.Alert("Error parsing file \"" + this._configFileName + "\" "+ex);
                return;
            }

            OnConfigLoaded(EventArgs.Empty);
        }

        private static StarDatabase ParseXml(XDocument xml)
        {
            var starNodes = xml.Root.Element("list").Elements("star");
            var type = typeof (StarInfo);

            var db = new StarDatabase();

            foreach (var node in starNodes)
            {
                var info = new StarInfo();
                foreach (var attr in node.Attributes())
                {
                    if(attr == null) continue;
                    var prop = type.GetProperty(attr.Name.ToString());
                    if (prop != null)
                    {
                        try
                        {
                            SetValue(attr.Value.Replace("−", "-"), prop, info);
                        }
                        catch(Exception ex)
                        {
                            throw new Exception("Cannot parse node " + node + " for attribute " + attr.Name + " with value " + attr.Value.Replace("−", "-"));
                        }
                    }
                }
                db.AddStarInfo(info);
            }

            db.StarsAdded();


            var lineNodes = xml.Root.Element("lines").Elements("connection");

            foreach (var line in lineNodes)
            {
                try
                {
                    db.AddLine(Int32.Parse(line.Attribute("from").Value), Int32.Parse(line.Attribute("to").Value));
                }
                catch(Exception)
                {
                    throw new XmlException("Cannot parse node " + line);
                }
            }

            return db;

        }

        private static void SetValue(string value, PropertyInfo prop, object obj)
        {
            var type = prop.PropertyType;
            if(type == typeof(double) || type == typeof(double?))
            {
                var d = Double.Parse(value);
                prop.SetValue(obj, d, null);
            }
            else if(type == typeof(int) || type == typeof(int?))
            {
                var i = value.StartsWith("0x") ? Int32.Parse(value.Replace("0x",""), NumberStyles.HexNumber) : Int32.Parse(value);
                prop.SetValue(obj, i, null);
            }
            else if (type == typeof(bool) || type == typeof(bool?))
            {
                var b = Boolean.Parse(value);
                prop.SetValue(obj, b, null);
            }
            else if (type == typeof(string))
            {
                prop.SetValue(obj, value, null);
            }
            else
            {
                prop.SetValue(obj, Convert.ChangeType(value, type, null), null);
            }
        }

        
    }
}
